{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6b82288e",
   "metadata": {},
   "source": [
    "# RAG 기본 구조 이해하기\n",
    "\n",
    "## 1. 사전작업(Pre-processing) - 1~4 단계\n",
    "\n",
    "![rag-1.png](./assets/rag-1.png)\n",
    "\n",
    "![rag-1-graphic](./assets/rag-graphic-1.png)\n",
    "\n",
    "사전 작업 단계에서는 데이터 소스를 Vector DB (저장소) 에 문서를 로드-분할-임베딩-저장 하는 4단계를 진행합니다.\n",
    "\n",
    "- 1단계 문서로드(Document Load): 문서 내용을 불러옵니다.\n",
    "- 2단계 분할(Text Split): 문서를 특정 기준(Chunk) 으로 분할합니다.\n",
    "- 3단계 임베딩(Embedding): 분할된(Chunk) 를 임베딩하여 저장합니다.\n",
    "- 4단계 벡터DB 저장: 임베딩된 Chunk 를 DB에 저장합니다.\n",
    "\n",
    "## 2. RAG 수행(RunTime) - 5~8 단계\n",
    "\n",
    "![rag-2.png](./assets/rag-2.png)\n",
    "\n",
    "![](./assets/rag-graphic-2.png)\n",
    "\n",
    "- 5단계 검색기(Retriever): 쿼리(Query) 를 바탕으로 DB에서 검색하여 결과를 가져오기 위하여 리트리버를 정의합니다. 리트리버는 검색 알고리즘이며(Dense, Sparse) 리트리버로 나뉘게 됩니다. Dense: 유사도 기반 검색, Sparse: 키워드 기반 검색\n",
    "- 6단계 프롬프트: RAG 를 수행하기 위한 프롬프트를 생성합니다. 프롬프트의 context 에는 문서에서 검색된 내용이 입력됩니다. 프롬프트 엔지니어링을 통하여 답변의 형식을 지정할 수 있습니다.\n",
    "- 7단계 LLM: 모델을 정의합니다.(GPT-3.5, GPT-4, Claude, etc..)\n",
    "- 8단계 Chain: 프롬프트 - LLM - 출력 에 이르는 체인을 생성합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1cf05522",
   "metadata": {},
   "source": [
    "## 실습에 활용한 문서\n",
    "\n",
    "소프트웨어정책연구소(SPRi) - 2023년 12월호\n",
    "\n",
    "- 저자: 유재흥(AI정책연구실 책임연구원), 이지수(AI정책연구실 위촉연구원)\n",
    "- 링크: https://spri.kr/posts/view/23669\n",
    "- 파일명: `SPRI_AI_Brief_2023년12월호_F.pdf`\n",
    "\n",
    "_실습을 위해 다운로드 받은 파일을 `data` 폴더로 복사해 주시기 바랍니다_\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01c423a8",
   "metadata": {},
   "source": [
    "## 환경설정\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a224fd32",
   "metadata": {},
   "source": [
    "API KEY 를 설정합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "418ab505",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0024d0c5",
   "metadata": {},
   "source": [
    "LangChain으로 구축한 애플리케이션은 여러 단계에 걸쳐 LLM 호출을 여러 번 사용하게 됩니다. 이러한 애플리케이션이 점점 더 복잡해짐에 따라, 체인이나 에이전트 내부에서 정확히 무슨 일이 일어나고 있는지 조사할 수 있는 능력이 매우 중요해집니다. 이를 위한 최선의 방법은 [LangSmith](https://smith.langchain.com)를 사용하는 것입니다.\n",
    "\n",
    "LangSmith가 필수는 아니지만, 유용합니다. LangSmith를 사용하고 싶다면, 위의 링크에서 가입한 후, 로깅 추적을 시작하기 위해 환경 변수를 설정해야 합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3edbbf89",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH12-RAG\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b0d050a",
   "metadata": {},
   "source": [
    "## RAG 기본 파이프라인(1~8단계)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3d1b0fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "from langchain_community.document_loaders import PyMuPDFLoader\n",
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.runnables import RunnablePassthrough\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "from langchain_openai import ChatOpenAI, OpenAIEmbeddings"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48e783c4",
   "metadata": {},
   "source": [
    "아래는 기본적인 RAG 구조 이해를 위한 뼈대코드(skeleton code) 입니다.\n",
    "\n",
    "각 단계별 모듈의 내용을 앞으로 상황에 맞게 변경하면서 문서에 적합한 구조를 찾아갈 수 있습니다.\n",
    "\n",
    "(각 단계별로 다양한 옵션을 설정하거나 새로운 기법을 적용할 수 있습니다.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "377894c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 1: 문서 로드(Load Documents)\n",
    "loader = PyMuPDFLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "docs = loader.load()\n",
    "print(f\"문서의 페이지수: {len(docs)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b34f4fe",
   "metadata": {},
   "source": [
    "페이지의 내용을 출력합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ddf0d7c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(docs[10].page_content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7e2963b",
   "metadata": {},
   "source": [
    "`metadata` 를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d6b05fd",
   "metadata": {},
   "outputs": [],
   "source": [
    "docs[10].__dict__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b52f26a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 2: 문서 분할(Split Documents)\n",
    "text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)\n",
    "split_documents = text_splitter.split_documents(docs)\n",
    "print(f\"분할된 청크의수: {len(split_documents)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "795cfec7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 3: 임베딩(Embedding) 생성\n",
    "embeddings = OpenAIEmbeddings()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "82f47754",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 4: DB 생성(Create DB) 및 저장\n",
    "# 벡터스토어를 생성합니다.\n",
    "vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "34dd3019",
   "metadata": {},
   "outputs": [],
   "source": [
    "for doc in vectorstore.similarity_search(\"구글\"):\n",
    "    print(doc.page_content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "838f7729",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 5: 검색기(Retriever) 생성\n",
    "# 문서에 포함되어 있는 정보를 검색하고 생성합니다.\n",
    "retriever = vectorstore.as_retriever()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f29da7b4",
   "metadata": {},
   "source": [
    "검색기에 쿼리를 날려 검색된 chunk 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16c0ad82",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 검색기에 쿼리를 날려 검색된 chunk 결과를 확인합니다.\n",
    "retriever.invoke(\"삼성전자가 자체 개발한 AI 의 이름은?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3bb3e26f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 6: 프롬프트 생성(Create Prompt)\n",
    "# 프롬프트를 생성합니다.\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"You are an assistant for question-answering tasks. \n",
    "Use the following pieces of retrieved context to answer the question. \n",
    "If you don't know the answer, just say that you don't know. \n",
    "Answer in Korean.\n",
    "\n",
    "#Context: \n",
    "{context}\n",
    "\n",
    "#Question:\n",
    "{question}\n",
    "\n",
    "#Answer:\"\"\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "669ed5b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 7: 언어모델(LLM) 생성\n",
    "# 모델(LLM) 을 생성합니다.\n",
    "llm = ChatOpenAI(model_name=\"gpt-4.1-mini\", temperature=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3113bc05",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 8: 체인(Chain) 생성\n",
    "chain = (\n",
    "    {\"context\": retriever, \"question\": RunnablePassthrough()}\n",
    "    | prompt\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e79f4aeb",
   "metadata": {},
   "source": [
    "생성된 체인에 쿼리(질문)을 입력하고 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50d6b7f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 체인 실행(Run Chain)\n",
    "# 문서에 대한 질의를 입력하고, 답변을 출력합니다.\n",
    "question = \"삼성전자가 자체 개발한 AI 의 이름은?\"\n",
    "response = chain.invoke(question)\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f8444e43",
   "metadata": {},
   "source": [
    "## 전체 코드"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "adc45dbf",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "from langchain_community.document_loaders import PyMuPDFLoader\n",
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.runnables import RunnablePassthrough\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
    "\n",
    "# 단계 1: 문서 로드(Load Documents)\n",
    "loader = PyMuPDFLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "docs = loader.load()\n",
    "\n",
    "# 단계 2: 문서 분할(Split Documents)\n",
    "text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)\n",
    "split_documents = text_splitter.split_documents(docs)\n",
    "\n",
    "# 단계 3: 임베딩(Embedding) 생성\n",
    "embeddings = OpenAIEmbeddings()\n",
    "\n",
    "# 단계 4: DB 생성(Create DB) 및 저장\n",
    "# 벡터스토어를 생성합니다.\n",
    "vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)\n",
    "\n",
    "# 단계 5: 검색기(Retriever) 생성\n",
    "# 문서에 포함되어 있는 정보를 검색하고 생성합니다.\n",
    "retriever = vectorstore.as_retriever()\n",
    "\n",
    "# 단계 6: 프롬프트 생성(Create Prompt)\n",
    "# 프롬프트를 생성합니다.\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"You are an assistant for question-answering tasks. \n",
    "Use the following pieces of retrieved context to answer the question. \n",
    "If you don't know the answer, just say that you don't know. \n",
    "Answer in Korean.\n",
    "\n",
    "#Context: \n",
    "{context}\n",
    "\n",
    "#Question:\n",
    "{question}\n",
    "\n",
    "#Answer:\"\"\"\n",
    ")\n",
    "\n",
    "# 단계 7: 언어모델(LLM) 생성\n",
    "# 모델(LLM) 을 생성합니다.\n",
    "llm = ChatOpenAI(model_name=\"gpt-4.1-mini\", temperature=0)\n",
    "\n",
    "# 단계 8: 체인(Chain) 생성\n",
    "chain = (\n",
    "    {\"context\": retriever, \"question\": RunnablePassthrough()}\n",
    "    | prompt\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5986cab",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 체인 실행(Run Chain)\n",
    "# 문서에 대한 질의를 입력하고, 답변을 출력합니다.\n",
    "question = \"삼성전자가 자체 개발한 AI 의 이름은?\"\n",
    "response = chain.invoke(question)\n",
    "print(response)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
